package org.script;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import com.xiaoleilu.hutool.log.Log;
import com.xiaoleilu.hutool.log.LogFactory;

public class ScriptJavaLoader {
    private final static Log log = LogFactory.get(ScriptJavaLoader.class);
    // private final Map<Integer, ScriptBean> scripts = new ConcurrentHashMap<>();
    /**
     * 所有脚本对象 key 脚本全类名 value ScriptBean
     */
    private final Map<String, ScriptBean> scriptBeanMap = new ConcurrentHashMap<>();
    private String classpath; // javac classpath
    private String javaFilePath; // .java文件路径
    private String classFilePath; // javac编译输出路径，内部ClassLoader查找脚本class文件路径

    private ScriptJavaLoader() {
        classpath = new String();
        javaFilePath = new String();
        classFilePath = new String();
    }

    public synchronized void initialize(String javaFilePath, String classFilePath) {
        this.javaFilePath = javaFilePath;
        this.classFilePath = classFilePath;
        buildClassPath();
    }

    /** 解析指定目录下java文件名 */
    public List<String> parseFolderJavaFile(String folderPath) {
        StringBuffer sb = new StringBuffer();
        sb.append(javaFilePath).append("/").append(folderPath.replace('.', '/'));
        File scriptRoot = new File(sb.toString());
        List<String> scriptNames = new ArrayList<>();
        parseClassName(scriptNames, folderPath, scriptRoot);
        return scriptNames;
    }

    private void parseClassName(List<String> scriptNames, String pkg, File file) {
        File[] childes = file.listFiles();
        file.getPath();
        for (File child : childes) {
            String fileName = child.getName();
            if (child.isFile() && fileName.endsWith(".java")) {
                String filePrefix = fileName.substring(0, fileName.indexOf("."));
                String name = pkg.isEmpty() ? filePrefix : pkg + "." + filePrefix;
                scriptNames.add(name);
            } else if (child.isDirectory()) {
                String pk = pkg.isEmpty() ? fileName : pkg + "." + fileName;
                parseClassName(scriptNames, pk, child);
            }
        }

    }

    /**
     * 加载脚本系统的全部脚本 scripts/java
     *
     * @param scriptNames
     */
    protected void loadScriptAll() {
        File scriptRoot = new File(javaFilePath);
        List<String> scriptNames = new ArrayList<>();
        parseClassName(scriptNames, "", scriptRoot);
        loadScript(scriptNames, true);
    }

    // /**
    // * 加载脚本
    // *
    // * @param scriptNames
    // */
    // public void loadScript(Map<Integer, String> scriptNames)
    // {
    // loadScript(parseCompileJavaFile(), true);
    // }



    // public void reloadScript(Map<Integer, String> scriptNames)
    // {
    // loadScript(parseCompileJavaFile(), true);
    // // loadScript(scriptNames, true);
    // }

    /**
     * 调用脚本
     *
     * @param scriptName
     * @param arg
     * @return
     */
    public Object call(String scriptName, Object arg) throws ScriptNotFoundException {
        IScript script = getScript(scriptName);
        if (script == null) {
            throw new ScriptNotFoundException("cannot find script, scriptId = " + scriptName);
        }
        return script.call(arg);
    }

    public static ScriptJavaLoader getInstance() {
        return Singleton.INSTANCE.getProcessor();
    }

    private ScriptBean getScriptBean(String scriptName) {
        return scriptBeanMap.get(scriptName);
    }

    public IScript getScript(String scriptName) {
        ScriptBean bean = getScriptBean(scriptName);
        return (bean != null) ? bean.getScript() : null;
    }

    /**
     * 判断是否正确设置ScriptLoader的相关path
     *
     * @return
     */
    private boolean hasAvailablePath() {
        return !classpath.isEmpty() && !javaFilePath.isEmpty() && !classFilePath.isEmpty();
    }

    private void buildClassPath() {
        try {
            File file = new File(this.classFilePath);
            if (file.exists()) {
                _deleteScriptPath(file);
            }
            file.mkdirs();

            URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();

            StringBuilder sb = new StringBuilder();
            for (URL url : classLoader.getURLs()) {
                String p = url.getFile();
                sb.append(p).append(File.pathSeparator);
            }
            sb.append(classFilePath);
            log.info("----classURLPath---------------------------------");
            log.info(sb.toString());
            log.info("----classURLPath---------------------------------");
            this.classpath = sb.toString();
            // 将当前类路径加入到类加载器中
            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            boolean accessible = method.isAccessible();

            if (accessible == false) {
                method.setAccessible(true);
            }
            method.invoke(classLoader, file.toURI().toURL());
            method.setAccessible(accessible);

        } catch (NoSuchMethodException | SecurityException | IllegalAccessException
                | IllegalArgumentException | InvocationTargetException | MalformedURLException ex) {
            log.error(ex);
            throw new RuntimeException(ex);
        }
    }

    /**
     * 加载脚本系统的全部脚本 使用foler|file进行指定目录
     *
     * @param scriptNames
     */
    public void loadScript(Map<String, String> parameters) {
        boolean loadAll = true;
        // 重载指定目录 eg: logic.marriage
        String folder = parameters.get("folder");
        if (folder != null) {
            String[] folders = folder.split(",");
            for (int i = 0; i < folders.length; i++) {
                loadScript(parseFolderJavaFile(folders[i]), true);
            }
            loadAll = false;
        }
        // 重载指定文件 eg: logic.marriage.AgreeMarriageApplyScript
        String file = parameters.get("file");
        if (file != null) {
            List<String> list = new ArrayList<String>();
            String[] files = file.split(",");
            for (int i = 0; i < files.length; i++) {
                list.add(files[i]);
            }
            loadScript(list, true);
            loadAll = false;
        }
        if (loadAll) {
            // 重载默认目录所有文件
            loadScriptAll();
        }
    }

    /**
     * 加载脚本 如果脚本class文件不存在则找相应的.java源文件编译后加载
     *
     * @param scriptNames
     * @param force 是否强制重新编译.java后加载
     */
    private void loadScript(List<String> scriptNames, boolean force) {

        if (!hasAvailablePath()) {
            throw new NullPointerException();
        }
        // Map<Integer,String> scriptIdNames = new HashMap<>();
        for (String scriptName : scriptNames) {
            // int scriptId = ((Integer) entry.getKey());
            // String scriptName = (String) entry.getValue();

            // ScriptBean scriptBean = scripts.get(scriptId); // 判断重新加载脚本
            // if (scriptBean != null)
            // {
            // log.warn("Reload script, scriptId = " + scriptId + " oldName = " +
            // scriptBean.getName() + " newName = " + scriptName);
            // }
            // else
            // {
            // scriptBean = new ScriptBean();
            // }

            if (!force) {
                try {

                    Class<?> clazz = Class.forName(scriptName); // 判断AppClassLoader是否能找到该脚本class文件
                    if (clazz != null) {
                        if (!IScript.class.isAssignableFrom(clazz)) {// 非IScript的实现
                            continue;
                        }
                        IScript script = (IScript) clazz.newInstance();
                        // Method method = clazz.getMethod("getId");
                        // int scriptId = (int) method.invoke(script);
                        // if(scriptIdNames.containsKey(scriptId)){
                        // log.error("重复的脚本ID[" + scriptId + "][new:" + scriptName + "]" + "[old:" +
                        // scriptIdNames.get(scriptId) + "]");
                        // continue;
                        // }
                        ScriptBean oldScriptBean = scriptBeanMap.get(scriptName);
                        if (oldScriptBean != null) {
                            IScript oldScript = oldScriptBean.getScript();
                            oldScript.destroy();
                        }
                        script.init();
                        ScriptBean scriptBean = new ScriptBean();
                        scriptBean.setId(0).setName(scriptName).setScript(script)
                                .setClassFileTimestamp(0).setJavaFileTimestamp(0);
                        // scripts.put(scriptId, scriptBean);
                        scriptBeanMap.put(scriptName, scriptBean);
                        // scriptIdNames.put(scriptId,scriptName);
                        continue;
                    }
                } catch (Exception e) {
                    log.error("AppClassLoader not fond class [" + scriptName + "]", e);
                }
            } else {

                try {
                    String fullJavaFilePath =
                            javaFilePath + "/" + scriptName.replace('.', '/') + ".java";
                    log.info("Begin Read java file [" + fullJavaFilePath + "]");
                    File file = new File(fullJavaFilePath);
                    if (file.isFile() && file.canRead()) {
                        InputStream inStream = new FileInputStream(fullJavaFilePath);
                        byte[] bytes = new byte[(int) file.length()];
                        inStream.read(bytes);
                        Class<?> clazz = javaCodeToObject(scriptName, new String(bytes, "UTF-8"));
                        if (clazz != null) {
                            if (!IScript.class.isAssignableFrom(clazz)) {// 非IScript的实现
                                log.error("非IScript的实现, scriptName:" + scriptName);
                                continue;
                            }
                            IScript script = (IScript) clazz.newInstance();
                            // Method method = clazz.getMethod("getId");
                            // int scriptId = (int) method.invoke(script);
                            // if(scriptIdNames.containsKey(scriptId)){
                            // log.error("重复的脚本ID["+scriptId+"][new:"+scriptName+"]"+"[old:"+scriptIdNames.get(scriptId)+"]");
                            // continue;
                            // }
                            ScriptBean oldScriptBean = scriptBeanMap.get(scriptName);
                            if (oldScriptBean != null) {
                                IScript oldScript = oldScriptBean.getScript();
                                oldScript.destroy();
                            }
                            script.init();
                            ScriptBean scriptBean = new ScriptBean();
                            scriptBean.setId(0).setName(scriptName).setScript(script)
                                    .setJavaFileTimestamp(file.lastModified())
                                    .setClassFileTimestamp(System.currentTimeMillis());
                            // scripts.put(scriptId, scriptBean);
                            scriptBeanMap.put(scriptName, scriptBean);
                            // scriptIdNames.put(scriptId,scriptName);
                        } else {
                            log.error("clazz is null, scriptName:" + scriptName);
                        }
                        log.info("End Read java file [" + fullJavaFilePath + "]");
                        inStream.close();
                    } else {
                        log.error(file.getPath() + " cannot read");
                    }
                } catch (Exception e) {
                    log.error(e);
                }
            }
        }

    }

    private Class<?> javaCodeToObject(String name, String code) throws IllegalAccessException,
            IOException, ClassNotFoundException {
        boolean reload = false;
        try {
            Class<?> c = Class.forName(name);
            reload = (c != null);
        } catch (ClassNotFoundException e) {
            // IDE添加了javasource目录则不会走这里
            // 现网:
            // 1. 1.4以前不一定,取决于谁打包,如何配置的 1.4-(maven打包导致)2.2不会走这里
            // 2. 其他现网版会有这句打印
            log.info("appClassloader can not find, load new script [" + name + "]");
        }

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        StandardJavaFileManager fileManager =
                compiler.getStandardFileManager(diagnostics, null, null);

        JavaFileObject jfile = new JavaSourceFromString(name, code);
        List<JavaFileObject> jfiles = new ArrayList<>();
        jfiles.add(jfile);
        List<String> options = new ArrayList<>();

        // 重新加载类需要用单独的类加载器
        ScriptClassLoader loader = new ScriptClassLoader();

        options.add("-encoding");
        options.add("UTF-8");
        options.add("-classpath");
        options.add(this.classpath);
        options.add("-d");
        options.add(classFilePath); // javac编译结果输出到classFilePath目录中

        JavaCompiler.CompilationTask task =
                compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);
        boolean success = task.call();
        fileManager.close();
        if (success) {
            if (reload) {
                try {
                    return loader.loadScriptClass(name);
                } catch (ClassNotFoundException e) {
                    log.error("ClassNotFoundException", e);
                }
            } else {
                return Class.forName(name);
            }
        } else {
            String error = "";
            for (Diagnostic<?> diagnostic : diagnostics.getDiagnostics()) {
                error = error + compilePrint(diagnostic);
            }
            log.error(error);
        }
        return null;
    }

    private class JavaSourceFromString extends SimpleJavaFileObject {

        private final String code;

        public JavaSourceFromString(String name, String code) {
            super(URI.create("string:///" + name.replace('.', '/')
                    + JavaFileObject.Kind.SOURCE.extension), JavaFileObject.Kind.SOURCE);
            this.code = code;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return code;
        }
    }

    private String compilePrint(Diagnostic<?> diagnostic) {
        StringBuilder res = new StringBuilder();
        res.append("Code:[").append(diagnostic.getCode()).append("]\n");
        res.append("Kind:[").append(diagnostic.getKind()).append("]\n");
        res.append("Position:[").append(diagnostic.getPosition()).append("]\n");
        res.append("Start Position:[").append(diagnostic.getStartPosition()).append("]\n");
        res.append("End Position:[").append(diagnostic.getEndPosition()).append("]\n");
        res.append("Source:[").append(diagnostic.getSource()).append("]\n");
        res.append("Message:[").append(diagnostic.getMessage(null)).append("]\n");
        res.append("LineNumber:[").append(diagnostic.getLineNumber()).append("]\n");
        res.append("ColumnNumber:[").append(diagnostic.getColumnNumber()).append("]\n");
        return res.toString();
    }

    private void _deleteScriptPath(File file) {
        if (file.isFile()) {
            file.delete();
        } else if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (int a = 0; a < files.length; ++a) {
                _deleteScriptPath(files[a]);
            }
        }
    }

    private class ScriptClassLoader extends ClassLoader {

        /**
         * 脚本字节码加载器 注意： 1 为了实现重新加载功能, 此处需打破JVM类加载器双亲委托模型, 否则始终由AppClassLoader加载将无法支持重新加载功能 2
         * 暂不支持脚本内部类
         *
         * @param name 类全名
         * @return
         * @throws ClassNotFoundException
         */
        public Class<?> loadScriptClass(String name) throws ClassNotFoundException {
            try {
                byte[] bytes = loadClassData(name);
                Class<?> clazz = this.defineClass(name, bytes, 0, bytes.length);
                return clazz;
            } catch (IOException e) {
                log.error(e);
                throw new ClassNotFoundException(name);
            }
        }

        /**
         * 读取字节码
         *
         * @param name
         * @return
         * @throws FileNotFoundException
         * @throws IOException
         */
        private byte[] loadClassData(String name) throws FileNotFoundException, IOException {
            int readCount = 0;
            String classFileName = classFilePath + "/" + name.replace('.', '/') + ".class";
            FileInputStream in = null;
            ByteArrayOutputStream buffer = null;
            try {
                in = new FileInputStream(classFileName);
                buffer = new ByteArrayOutputStream();
                while ((readCount = in.read()) != -1) {
                    buffer.write(readCount);
                }
                return buffer.toByteArray();
            } finally {
                try {
                    if (in != null) {
                        in.close();
                    }
                    if (buffer != null) {
                        buffer.close();
                    }
                } catch (IOException e) {
                    log.error(e);
                }
            }
        }
    }

    private enum Singleton {

        INSTANCE;
        ScriptJavaLoader loader;

        Singleton() {
            this.loader = new ScriptJavaLoader();
        }

        ScriptJavaLoader getProcessor() {
            return loader;
        }
    }
}
